/*******************************************************************************
 * Copyright (c) 2002, 2003 David Corbin and others. All rights reserved. This
 * program and the accompanying materials are made available under the terms of
 * the Common Public License v0.5 which accompanies this distribution, and is
 * available at http://www.eclipse.org/legal/cpl-v05.html
 * 
 * Contributors: David Corbin - provides access to a Perforce server abstraction
 * Boris Pruessmann - transformation from PerforceServerFactory to
 * PerforceServerManager
 ******************************************************************************/
package net.sourceforge.perforce.core.internal.api;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import net.sourceforge.perforce.api.CharSetEnum;
import net.sourceforge.perforce.api.IClientApi;
import net.sourceforge.perforce.api.PerforceApiPlugin;
import net.sourceforge.perforce.core.PerforceCorePlugin;
import net.sourceforge.perforce.core.PerforceException;
import net.sourceforge.perforce.core.api.IPerforceListener;
import net.sourceforge.perforce.core.api.IPerforceServer;
import net.sourceforge.perforce.core.api.IPerforceServerManager;
import net.sourceforge.perforce.core.internal.P4ConfigLoader;
import net.sourceforge.perforce.core.internal.P4TeamProvider;
import net.sourceforge.perforce.core.internal.Policy;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.team.core.RepositoryProvider;
import org.eclipse.team.core.TeamException;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @version $Revision: 488 $
 * @author <a href="mailto:dcorbin@users.sourceforge.net">David Corbin</a>
 * @author <a href="mailto:bpruessmann@users.sourceforge.net">Boris
 *         Pruessmann</a>
 */
public class PerforceServerManager implements IPerforceServerManager {
  // ---------------------------------------------------------------------------
  // Private Constants

  /** Name of the file storing the state of this class. */
  private static final String STATE_FILE = ".perforceServeryManagerState"; //$NON-NLS-1$

  /** Constant indicating the version of the server state file. */
  private static final int VERSION = 3;

  // ---------------------------------------------------------------------------
  // Private Variables

  /** Map that stores the uniqueName -> server mapping. */
  private Map<String, IPerforceServer> servers = Maps.newHashMap();
  /** Stores the registered listeners. */
  private List<IPerforceListener> listeners = new ArrayList<IPerforceListener>();

  // ----------------------------------------------------- Public Methods
  // (IPerforceServerManager)

  public IPerforceServer createPerforceServer(Map<String, ?> properties) throws PerforceException {
    String port = (String) properties.get("port"); //$NON-NLS-1$
    String client = (String) properties.get("client"); //$NON-NLS-1$
    String host = (String) properties.get("host"); //$NON-NLS-1$
    String user = (String) properties.get("user"); //$NON-NLS-1$
    String password = (String) properties.get("password"); //$NON-NLS-1$
    String charset = (String) properties.get("charset"); //$NON-NLS-1$
    Boolean backgroundUpdates = (Boolean) properties.get("backgroundUpdates"); //$NON-NLS-1$

    IPerforceServer server =
        new PerforceServer(port, client, host, user, password, CharSetEnum.fromString(charset),
            backgroundUpdates.booleanValue());
    IPerforceServer existingServer = servers.get(server.getUniqueName());
    if (existingServer != null) {
      String message = Policy.bind("PerforceServerManager.alreadyExists"); //$NON-NLS-1$

      throw new PerforceException(new Status(Status.ERROR, PerforceCorePlugin.ID, 0, message, null));
    }

    return server;
  }

  /** @see IPerforceServerManager#addPerforceServer(IPerforceServer) */
  public void addPerforceServer(IPerforceServer server) throws PerforceException {
    IPerforceServer existingServer = servers.get(server.getUniqueName());
    if (existingServer != null) {
      ((PerforceServer) server).updateCache();
    } else {
      synchronized (servers) {
        servers.put(server.getUniqueName(), server);
      }
      perforceServerAdded(server);
      ((PerforceServer) server).updateCache();
    }
  }

  /** @see IPerforceServerManager#removePerforceServer(IPerforceServer) */
  public void removePerforceServer(IPerforceServer server) {
    IPerforceServer existingServer = servers.get(server.getUniqueName());
    if (existingServer == null) {
      return;
    } else {
      synchronized (servers) {
        servers.remove(server.getUniqueName());
      }
      perforceServerRemoved(server);
    }
  }


  /** @see IPerforceServerManager#getPerforceServers() */
  public IPerforceServer[] getPerforceServers() {
    return servers.values().toArray(new IPerforceServer[servers.size()]);
  }

  /**
   * Returns connection defaults honoring the P4CONFIG environmental variable,
   * and values found in the file[s] named in that variable.
   */
  public Map<String, Object> getConnectionDefaults(IPath path) {
    Map<String, Object> defaults = getConnectionDefaults();

    // TODO: need tests for conditions in which the path is defined
    // but entirely wonky.
    if (path != null) {
      String p4config = System.getenv("P4CONFIG"); //$NON-NLS-1$
      if (p4config != null) {
        // find config files and apply their settings to our defaults.
        new P4ConfigLoader(path.toFile(), p4config).applySettings(defaults);
      }
    }
    return defaults;
  }

  public Map<String, Object> getConnectionDefaults() {
    try {
      IClientApi client = PerforceApiPlugin.createClientApi();

      Map<String, Object> defaults = Maps.newHashMap();
      defaults.put("port", client.getPort());
      defaults.put("client", client.getClient());
      defaults.put("host", client.getHost());
      defaults.put("user", client.getUser());
      defaults.put("charset", client.getCharset());

      return defaults;
    } catch (Error re) {
      PerforceCorePlugin.logError("Failed to create ClientApi", re);
      throw re;
    }
  }

  /**
   * @see IPerforceServerManager#setSharing(IProject, IPerforceServer,
   *      IProgressMonitor)
   */
  public void setSharing(IProject project, IPerforceServer server, IProgressMonitor monitor)
      throws PerforceException {
    try {
      project.setPersistentProperty(QN_PERSISTENT_SERVER, server.getUniqueName());

      if (!project.hasNature(PerforceCorePlugin.getTypeId())) {
        RepositoryProvider.map(project, PerforceCorePlugin.getTypeId());
      }
    } catch (TeamException e) {
      throw new PerforceException(e.getStatus());
    } catch (CoreException e) {
      throw new PerforceException(e.getStatus());
    }
  }

  public IPerforceServer getAssociatedPerforceServer(IProject project) throws PerforceException {
    try {
      String uniqueName = project.getPersistentProperty(QN_PERSISTENT_SERVER);
      if (uniqueName != null) {
        return servers.get(uniqueName);
      }

      return null;
    } catch (CoreException e) {
      throw new PerforceException(e.getStatus());
    }
  }

  public IPerforceServer getPerforceServer(IResource resource) throws PerforceException {
    try {
      P4TeamProvider provider = P4TeamProvider.getProvider(resource);
      if (provider != null) {
        IProject project = resource.getProject();
        String uniqueName = project.getPersistentProperty(QN_PERSISTENT_SERVER);
        if (uniqueName != null) {
          return servers.get(uniqueName);
        }
      }

      return null;
    } catch (CoreException e) {
      throw new PerforceException(e.getStatus());
    }
  }

  public IPerforceServer getPerforceServer(String uniqueName) {
    return servers.get(uniqueName);
  }

  /** @see IPerforceServerManager#getReferencingProjects(IPerforceServer) */
  public IProject[] getReferencingProjects(IPerforceServer repository) throws PerforceException {
    IProject[] projects = PerforceCorePlugin.getWorkspace().getRoot().getProjects();

    List<IProject> result = Lists.newArrayListWithExpectedSize(projects.length);
    try {
      for (int i = 0, length = projects.length; i < length; i++) {
        P4TeamProvider repositoryProvider = P4TeamProvider.getProvider((IResource) projects[i]);

        if (repositoryProvider != null) {
          String uniqueName = projects[i].getPersistentProperty(QN_PERSISTENT_SERVER);
          if (uniqueName != null && uniqueName.equals(repository.getUniqueName())) {
            result.add(projects[i]);
          }
        }
      }
    } catch (CoreException e) {
      throw new PerforceException(e.getStatus());
    }

    return result.toArray(new IProject[result.size()]);
  }

  /** @see IPerforceServerManager#addPerforceListener(IPerforceListener) */
  public void addPerforceListener(IPerforceListener listener) {
    listeners.add(listener);
  }

  /** @see IPerforceServerManager#removePerforceListener(IPerforceListener) */
  public void removePerforceListener(IPerforceListener listener) {
    listeners.remove(listener);
  }

  //----------------------------------------------------------------------------
  // -- Public Methods

  /**
   * Called to perform startup operations like loading the known server.
   * 
   * @throws PerforceException in case of an error.
   */
  public void startup() throws PerforceException {
    loadState();
  }

  /**
   * Called to perform shutdown operations like storing the servers.
   * 
   * @throws PerforceException in case of an error.
   */
  public void shutdown() throws PerforceException {
    saveState();
  }

  /**
   * Called to save the server manager's current state.
   * 
   * @throws PerforceException in case of an error.
   */
  public void performSnapshot() throws PerforceException {
    saveState();
  }

  //----------------------------------------------------------------------------
  // - Private Methods

  /**
   * Called to load the last saved state of the server manager from disk.
   * 
   * @throws PerforceException in case of an error.
   */
  private void loadState() throws PerforceException {
    IPath pluginStateLocation = PerforceCorePlugin.getPlugin().getStateLocation();

    File file = pluginStateLocation.append(STATE_FILE).toFile();
    if (file.exists()) {
      try {
        DataInputStream dis = new DataInputStream(new FileInputStream(file));
        try {
          readState(dis);
        } finally {
          dis.close();
        }
      } catch (IOException e) {
        PerforceCorePlugin.log(new Status(Status.ERROR, PerforceCorePlugin.ID,
            TeamException.UNABLE, Policy.bind("PerforceServerManager.ioException"), e)); //$NON-NLS-1$
      } catch (PerforceException e) {
        PerforceCorePlugin.log(e.getStatus());
      }
    }

  }

  /**
   * Reads the state of the PerforceServerManager from the given
   * DataInputStream.
   * 
   * @param dis the DataInputStream.
   * 
   * @throws IOException in case of an I/O Error.
   * @throws PerforceException in case server creation failed.
   */
  private void readState(DataInputStream dis) throws IOException, PerforceException {
    int version = 0;
    int serverCount = dis.readInt();
    if (serverCount == -1) {
      version = dis.readInt();
      serverCount = dis.readInt();
    }

    for (int i = 0; i < serverCount; i++) {
      Map<String, Object> config = Maps.newHashMap();
      config.put("port", dis.readUTF()); //$NON-NLS-1$
      config.put("client", dis.readUTF()); //$NON-NLS-1$
      config.put("user", dis.readUTF()); //$NON-NLS-1$

      if (version > 0) {
        config.put("backgroundUpdates", new Boolean(dis.readBoolean())); //$NON-NLS-1$
      } else {
        config.put("backgroundUpdates", Boolean.TRUE); //$NON-NLS-1$
      }

      if (version > 1) {
        config.put("charset", dis.readUTF());
      } else {
        config.put("charset", "");
      }

      if (version > 2) {
        config.put("host", dis.readUTF());
      } else {
        config.put("host", "");
      }

      IPerforceServer server = createPerforceServer(config);
      addPerforceServer(server);
    }
  }

  /**
   * Called to store the currently configured servers.
   * 
   * @throws PerforceException in case of an error.
   */
  private void saveState() throws PerforceException {
    IPath pluginStateLocation = PerforceCorePlugin.getPlugin().getStateLocation();

    File tempFile = pluginStateLocation.append(STATE_FILE + ".tmp").toFile(); //$NON-NLS-1$
    File stateFile = pluginStateLocation.append(STATE_FILE).toFile();
    try {
      DataOutputStream dos = new DataOutputStream(new FileOutputStream(tempFile));
      try {
        writeState(dos);
      } finally {
        dos.close();
      }

      if (stateFile.exists()) {
        stateFile.delete();
      }

      boolean renamed = tempFile.renameTo(stateFile);
      if (!renamed) {
        throw new PerforceException(new Status(Status.ERROR, PerforceCorePlugin.ID,
            TeamException.UNABLE, Policy.bind("PerforceServerManager.rename", //$NON-NLS-1$
                tempFile.getAbsolutePath()), null));
      }
    } catch (IOException e) {
      throw new PerforceException(new Status(Status.ERROR, PerforceCorePlugin.ID,
          TeamException.UNABLE, Policy.bind("PerforceServerManager.save", //$NON-NLS-1$
              stateFile.getAbsolutePath()), e));
    }
  }

  /**
   * Write the state of the PerforceServerManager to the given DataOutputStream.
   * 
   * @param dos used for output.
   * 
   * @throws IOException in case of an I/O error.
   */
  private void writeState(DataOutputStream dos) throws IOException {
    dos.writeInt(-1);
    dos.writeInt(VERSION);

    IPerforceServer[] servers = getPerforceServers();
    dos.writeInt(servers.length);

    for (IPerforceServer server : servers) {
      dos.writeUTF(server.getPort());
      dos.writeUTF(server.getClient());
      dos.writeUTF(server.getUser());
      dos.writeBoolean(server.shouldIncludeInBackgroundUpdates());

      // Version 2
      dos.writeUTF(server.getCharset().toString());

      // Version 3
      dos.writeUTF(server.getHost());
    }
  }

  /**
   * Called to fire a perforceServerAdded event notification.
   * 
   * @param server the server that has been added.
   */
  private void perforceServerAdded(IPerforceServer server) {
    for (IPerforceListener listener : listeners) {
      listener.perforceServerAdded(server);
    }
  }

  /**
   * Called to fire a perforceServerRemoved event notification.
   * 
   * @param server the server that has been removed.
   */
  private void perforceServerRemoved(IPerforceServer server) {
    for (IPerforceListener listener : listeners) {
      listener.perforceServerRemoved(server);
    }
  }
}
